/*
 *
 * Copyright Eclub organization.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.eclub.emotion.rootstone.serializer.scanning;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.ReflectPermission;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.eclub.emotion.rootstone.server.ResourceFinder;
import com.eclub.emotion.rootstone.util.ReflectionHelper;
import com.eclub.emotion.rootstone.util.Tokenizer;

/**
 * 
 * A scanner that recursively scans URI-based resources present in a set of
 * package names, and sub-package names of that set.
 * <p>
 * The URIs for a package name are obtained, by default, by invoking
 * {@link ClassLoader#getResources(java.lang.String) } with the parameter that is
 * the package name with "." replaced by "/".
 * <p>
 * Each URI is then scanned using a registered
 * {@link UriSchemeResourceFinderFactory} that supports the URI scheme.
 * <p>
 * The following are registered by default. The
 * {@link FileSchemeResourceFinderFactory} for "file" URI schemes. The
 * {@link JarZipSchemeResourceFinderFactory} for "jar" or "zip" URI schemes to
 * jar resources. The {@link VfsSchemeResourceFinderFactory} for the JBoss-based
 * "vfsfile" and "vfszip" URI schemes.
 * <p>
 * Further schemes may be registered by registering an implementation of
 * {@link UriSchemeResourceFinderFactory} in the META-INF/services file whose
 * name is the the fully qualified class name of
 * {@link UriSchemeResourceFinderFactory}.
 * <p>
 * If a URI scheme is not supported a {@link ResourceFinderException} will be
 * thrown and package scanning deployment will fail.
 * 
 * @author Paul Sandoz
 * @author Jakub Podlesak (jakub.podlesak at oracle.com)
 * @author jun.yang, Wincor Nixdorf International GmbH
 */
public class PackageNamesScanner implements ResourceFinder {

	private final String[] packages;
	private final ClassLoader classloader;
	private final Map<String, UriSchemeResourceFinderFactory> finderFactories;

	private ResourceFinderStack resourceFinderStack;

	/**
	 * Scan from a set of packages using the context {@link ClassLoader}.
	 * 
	 * @param packages
	 *            an array of package names.
	 */
	public PackageNamesScanner(final String[] packages) {
		this(ReflectionHelper.getContextClassLoader(), Tokenizer.tokenize(packages, Tokenizer.COMMON_DELIMITERS));
	}

	/**
	 * Scan from a set of packages using provided {@link ClassLoader}.
	 * 
	 * @param classloader
	 *            the {@link ClassLoader} to load classes from.
	 * @param packages
	 *            an array of package names.
	 */
	public PackageNamesScanner(final ClassLoader classloader, final String[] packages) {
		this.packages = packages;
		this.classloader = classloader;

		this.finderFactories = new HashMap<String, UriSchemeResourceFinderFactory>();
		add(new JarZipSchemeResourceFinderFactory());
		add(new FileSchemeResourceFinderFactory());
		init();
	}

	private void add(final UriSchemeResourceFinderFactory uriSchemeResourceFinderFactory) {
		for (final String s : uriSchemeResourceFinderFactory.getSchemes()) {
			finderFactories.put(s.toLowerCase(), uriSchemeResourceFinderFactory);
		}
	}

	@Override
	public boolean hasNext() {
		return resourceFinderStack.hasNext();
	}

	@Override
	public String next() {
		return resourceFinderStack.next();
	}

	@Override
	public void remove() {
		resourceFinderStack.remove();
	}

	@Override
	public InputStream open() {
		return resourceFinderStack.open();
	}

	@Override
	public void reset() {
		init();
	}

	private void init() {
		resourceFinderStack = new ResourceFinderStack();

		for (final String p : packages) {
			try {
				final Enumeration<URL> urls = ResourcesProvider.getInstance().getResources(p.replace('.', '/'), classloader);
				while (urls.hasMoreElements()) {
					try {
						addResourceFinder(toURI(urls.nextElement()));
					} catch (URISyntaxException e) {
						throw new ResourceFinderException("Error when converting a URL to a URI", e);
					}
				}
			} catch (IOException e) {
				throw new ResourceFinderException("IO error when package scanning jar", e);
			}
		}

	}

	/**
	 * Find resources with a given name and class loader.
	 */
	public static abstract class ResourcesProvider {

		private static volatile ResourcesProvider provider;

		private static ResourcesProvider getInstance() {
			// Double-check idiom for lazy initialization
			ResourcesProvider result = provider;

			if (result == null) { // first check without locking
				synchronized (ResourcesProvider.class) {
					result = provider;
					if (result == null) { // second check with locking
						provider = result = new ResourcesProvider() {

							@Override
							public Enumeration<URL> getResources(String name, ClassLoader cl) throws IOException {
								return cl.getResources(name);
							}
						};

					}
				}

			}
			return result;
		}

		private static void setInstance(ResourcesProvider provider) throws SecurityException {
			SecurityManager security = System.getSecurityManager();
			if (security != null) {
				ReflectPermission rp = new ReflectPermission("suppressAccessChecks");
				security.checkPermission(rp);
			}
			synchronized (ResourcesProvider.class) {
				ResourcesProvider.provider = provider;
			}
		}

		/**
		 * Find all resources with the given name using a class loader.
		 * 
		 * @param cl
		 *            the class loader use to find the resources
		 * @param name
		 *            the resource name
		 * @return An enumeration of URL objects for the resource. If no
		 *         resources could be found, the enumeration will be empty.
		 *         Resources that the class loader doesn't have access to will
		 *         not be in the enumeration.
		 * @throws IOException
		 *             if I/O errors occur
		 */
		public abstract Enumeration<URL> getResources(String name, ClassLoader cl) throws IOException;
	}

	/**
	 * Set the {@link ResourcesProvider} implementation to find resources.
	 * <p>
	 * This method should be invoked before any package scanning is performed
	 * otherwise the functionality method will be utilized.
	 * 
	 * @param provider
	 *            the resources provider.
	 * @throws SecurityException
	 *             if the resources provider cannot be set.
	 */
	public static void setResourcesProvider(ResourcesProvider provider) throws SecurityException {
		ResourcesProvider.setInstance(provider);
	}

	private void addResourceFinder(final URI u) {
		final UriSchemeResourceFinderFactory finderFactory = finderFactories.get(u.getScheme().toLowerCase());
		if (finderFactory != null) {
			resourceFinderStack.push(finderFactory.create(u));
		} else {
			throw new ResourceFinderException("The URI scheme " + u.getScheme() + " of the URI " + u + " is not supported. Package scanning deployment is not" + " supported for such URIs." + "\nTry using a different deployment mechanism such as" + " explicitly declaring root resource and provider classes" + " using an extension of javax.ws.rs.core.Application");
		}
	}

	private URI toURI(URL url) throws URISyntaxException {
		try {
			return url.toURI();
		} catch (URISyntaxException e) {
			// Work around bug where some URLs are incorrectly encoded.
			// This can occur when certain class loaders are utilized
			// to obtain URLs for resources.
			return URI.create(toExternalForm(url));
		}
	}

	private String toExternalForm(URL u) {

		// pre-compute length of StringBuffer
		int len = u.getProtocol().length() + 1;
		if (u.getAuthority() != null && u.getAuthority().length() > 0) {
			len += 2 + u.getAuthority().length();
		}
		if (u.getPath() != null) {
			len += u.getPath().length();
		}
		if (u.getQuery() != null) {
			len += 1 + u.getQuery().length();
		}
		if (u.getRef() != null) {
			len += 1 + u.getRef().length();
		}

		StringBuilder result = new StringBuilder(len);
		result.append(u.getProtocol());
		result.append(":");
		if (u.getAuthority() != null && u.getAuthority().length() > 0) {
			result.append("//");
			result.append(u.getAuthority());
		}
		if (u.getPath() != null) {
			result.append(_encode(u.getPath(), Type.PATH, false, true));
		}
		if (u.getQuery() != null) {
			result.append('?');
			result.append(_encode(u.getQuery(), Type.QUERY, false, true));
		}
		if (u.getRef() != null) {
			result.append("#");
			result.append(u.getRef());
		}
		return result.toString();
	}

	public enum Type {

		/**
		 * ALPHA / DIGIT / "-" / "." / "_" / "~" characters
		 */
		UNRESERVED,
		/**
		 * The URI scheme component type.
		 */
		SCHEME,
		/**
		 * The URI authority component type.
		 */
		AUTHORITY,
		/**
		 * The URI user info component type.
		 */
		USER_INFO,
		/**
		 * The URI host component type.
		 */
		HOST,
		/**
		 * The URI port component type.
		 */
		PORT,
		/**
		 * The URI path component type.
		 */
		PATH,
		/**
		 * The URI path component type that is a path segment.
		 */
		PATH_SEGMENT,
		/**
		 * The URI path component type that is a matrix parameter.
		 */
		MATRIX_PARAM,
		/**
		 * The URI query component type.
		 */
		QUERY,
		/**
		 * The URI query component type that is a query parameter.
		 */
		QUERY_PARAM,
		/**
		 * The URI fragment component type.
		 */
		FRAGMENT,
	}

	private static final String[] SCHEME = { "0-9", "A-Z", "a-z", "+", "-", "." };
	private static final String[] UNRESERVED = { "0-9", "A-Z", "a-z", "-", ".", "_", "~" };
	private static final String[] SUB_DELIMS = { "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" };
	private final boolean[][] ENCODING_TABLES = initEncodingTables();

	private boolean[][] initEncodingTables() {
		boolean[][] tables = new boolean[Type.values().length][];

		List<String> l = new ArrayList<String>();
		l.addAll(Arrays.asList(SCHEME));
		tables[Type.SCHEME.ordinal()] = initEncodingTable(l);

		l.clear();

		l.addAll(Arrays.asList(UNRESERVED));
		tables[Type.UNRESERVED.ordinal()] = initEncodingTable(l);

		l.addAll(Arrays.asList(SUB_DELIMS));

		tables[Type.HOST.ordinal()] = initEncodingTable(l);

		tables[Type.PORT.ordinal()] = initEncodingTable(Arrays.asList("0-9"));

		l.add(":");

		tables[Type.USER_INFO.ordinal()] = initEncodingTable(l);

		l.add("@");

		tables[Type.AUTHORITY.ordinal()] = initEncodingTable(l);

		tables[Type.PATH_SEGMENT.ordinal()] = initEncodingTable(l);
		tables[Type.PATH_SEGMENT.ordinal()][';'] = false;

		tables[Type.MATRIX_PARAM.ordinal()] = tables[Type.PATH_SEGMENT.ordinal()].clone();
		tables[Type.MATRIX_PARAM.ordinal()]['='] = false;

		l.add("/");

		tables[Type.PATH.ordinal()] = initEncodingTable(l);

		l.add("?");

		tables[Type.QUERY.ordinal()] = initEncodingTable(l);

		tables[Type.FRAGMENT.ordinal()] = tables[Type.QUERY.ordinal()];

		tables[Type.QUERY_PARAM.ordinal()] = initEncodingTable(l);
		tables[Type.QUERY_PARAM.ordinal()]['='] = false;
		tables[Type.QUERY_PARAM.ordinal()]['+'] = false;
		tables[Type.QUERY_PARAM.ordinal()]['&'] = false;

		return tables;
	}

	private boolean[] initEncodingTable(List<String> allowed) {
		boolean[] table = new boolean[0x80];
		for (String range : allowed) {
			if (range.length() == 1) {
				table[range.charAt(0)] = true;
			} else if (range.length() == 3 && range.charAt(1) == '-') {
				for (int i = range.charAt(0); i <= range.charAt(2); i++) {
					table[i] = true;
				}
			}
		}

		return table;
	}

	private final int[] HEX_TABLE = initHexTable();

	private int[] initHexTable() {
		int[] table = new int[0x80];
		Arrays.fill(table, -1);

		for (char c = '0'; c <= '9'; c++) {
			table[c] = c - '0';
		}
		for (char c = 'A'; c <= 'F'; c++) {
			table[c] = c - 'A' + 10;
		}
		for (char c = 'a'; c <= 'f'; c++) {
			table[c] = c - 'a' + 10;
		}
		return table;
	}

	private boolean isHexCharacter(char c) {
		return c < 128 && HEX_TABLE[c] != -1;
	}

	private String _encode(String s, Type t, boolean template, boolean contextualEncode) {
		final boolean[] table = ENCODING_TABLES[t.ordinal()];

		StringBuilder sb = null;
		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);
			if (c < 0x80 && table[c]) {
				if (sb != null) {
					sb.append(c);
				}
			} else {
				if (template && (c == '{' || c == '}')) {
					if (sb != null) {
						sb.append(c);
					}
					continue;
				} else if (contextualEncode) {
					if (c == '%' && i + 2 < s.length()) {
						if (isHexCharacter(s.charAt(i + 1)) && isHexCharacter(s.charAt(i + 2))) {
							if (sb != null) {
								sb.append('%').append(s.charAt(i + 1)).append(s.charAt(i + 2));
							}
							i += 2;
							continue;
						}
					}
				}

				if (sb == null) {
					sb = new StringBuilder();
					sb.append(s.substring(0, i));
				}

				if (c < 0x80) {
					if (c == ' ' && (t == Type.QUERY_PARAM)) {
						sb.append('+');
					} else {
						appendPercentEncodedOctet(sb, c);
					}
				} else {
					appendUTF8EncodedCharacter(sb, c);
				}
			}
		}

		return (sb == null) ? s : sb.toString();
	}

	private final static char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

	private void appendPercentEncodedOctet(StringBuilder sb, int b) {
		sb.append('%');
		sb.append(HEX_DIGITS[b >> 4]);
		sb.append(HEX_DIGITS[b & 0x0F]);
	}

	private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");

	private void appendUTF8EncodedCharacter(StringBuilder sb, char c) {
		final ByteBuffer bb = UTF8_CHARSET.encode("" + c);

		while (bb.hasRemaining()) {
			appendPercentEncodedOctet(sb, bb.get() & 0xFF);
		}
	}

}
